<!DOCTYPE html>
<html>

<head>
    
    <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">

<meta name="description" content="A new Hugo site.">
<title>
    golang下map的性能分析 - dashjay
</title>



<link rel="shortcut icon" href="/sam.ico">








<link rel="stylesheet" href="/css/main.min.4321edce0de245f4b1d32680d89ac669940fe5dec17e1791c721d9a0954987b4.css" integrity="sha256-QyHtzg3iRfSx0yaA2JrGaZQP5d7BfheRxyHZoJVJh7Q=" crossorigin="anonymous" media="screen">





<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Didact+Gothic">

<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="http://dashjay.gitee.io/tn.png"/>

<meta name="twitter:title" content="golang下map的性能分析"/>
<meta name="twitter:description" content="golang 中 map 性能优化[低阶] 简单介绍 golang 中的 build-in 的 map 这个 map 是非线程安全的，但是也是最常用的一个家伙。 为了测试多个 map 的性能我写了个接口 Map
type Map interface { Set(key interface{}, val interface{}) Get(key interface{}) (interface{}, bool) Del(key interface{}) } 然后这是封装的普通的 map
type OriginMap struct { m map[interface{}]interface{} } func NewOriginMap() *OriginMap { return &amp;OriginMap{m: make(map[interface{}]interface{})} } func (o *OriginMap) Get(key interface{}) (interface{}, bool) { v, ok := o.m[key] return v, ok } func (o *OriginMap) Set(key interface{}, value interface{}) { o."/>

<meta property="og:title" content="golang下map的性能分析" />
<meta property="og:description" content="golang 中 map 性能优化[低阶] 简单介绍 golang 中的 build-in 的 map 这个 map 是非线程安全的，但是也是最常用的一个家伙。 为了测试多个 map 的性能我写了个接口 Map
type Map interface { Set(key interface{}, val interface{}) Get(key interface{}) (interface{}, bool) Del(key interface{}) } 然后这是封装的普通的 map
type OriginMap struct { m map[interface{}]interface{} } func NewOriginMap() *OriginMap { return &amp;OriginMap{m: make(map[interface{}]interface{})} } func (o *OriginMap) Get(key interface{}) (interface{}, bool) { v, ok := o.m[key] return v, ok } func (o *OriginMap) Set(key interface{}, value interface{}) { o." />
<meta property="og:type" content="article" />
<meta property="og:url" content="http://dashjay.gitee.io/posts/golang-map/" />
<meta property="og:image" content="http://dashjay.gitee.io/tn.png"/>
<meta property="article:published_time" content="2020-01-12T00:00:00+00:00" />
<meta property="article:modified_time" content="2020-01-12T00:00:00+00:00" /><meta property="og:site_name" content="Call me Sam" />


    

    
    
    
    <title>
        
        golang下map的性能分析
        
    </title>
</head>

<body>
    
    
    <header class="wrap flex-container">
        <h1>golang下map的性能分析</h1>
    </header>
    
    <main class="wrap">
        
<div class="flex-container">
    <aside role="complementary">
        Sun Jan 12, 2020 &#183; 733 words
        <div class="tag-container">
            
            
            <span class="tag">
                <a href="/tags/golang/">
                    golang
                </a>
            </span>
            
            
        </div>
    </aside>
    <hr />
    <article role="article">
        <h1 id="golang-中-map-性能优化低阶">golang 中 map 性能优化[低阶]</h1>
<h2 id="简单介绍">简单介绍</h2>
<p>golang 中的 build-in 的 map 这个 map 是非线程安全的，但是也是最常用的一个家伙。 为了测试多个 map 的性能我写了个接口 Map</p>
<pre><code>type Map interface {
    Set(key interface{}, val interface{})
    Get(key interface{}) (interface{}, bool)
    Del(key interface{})
}
</code></pre><p>然后这是封装的普通的 map</p>
<pre><code>type OriginMap struct {
    m map[interface{}]interface{}
}

func NewOriginMap() *OriginMap {
    return &amp;OriginMap{m: make(map[interface{}]interface{})}
}

func (o *OriginMap) Get(key interface{}) (interface{}, bool) {
    v, ok := o.m[key]
    return v, ok
}
func (o *OriginMap) Set(key interface{}, value interface{}) {
    o.m[key] = value
}

func (o *OriginMap) Del(key interface{}) {
    delete(o.m, key)
}
</code></pre><p>别看一堆代码，其实就是 get 和 set 操作。在这里我们要使用 golang 自带的 test 工具</p>
<pre><code>func TestOriginMaps(t *testing.T) {
    hm := NewOriginMap()
    wg := sync.WaitGroup{}
    for i := 0; i &lt; Writer; i++ {
        wg.Add(1)
        go func(wg *sync.WaitGroup) {
            for k := 0; k &lt; 100; k++ {
                hm.Set(strconv.Itoa(k), k*k)
                val, _ := hm.Get(strconv.Itoa(k))
                t.Logf(&quot;Get %d = %d&quot;, k, val)
            }
            wg.Done()
        }(&amp;wg)
    }
    wg.Wait()
}
</code></pre><p>这其中有个变量 Writer 就是写者的数量，如果只有 1 的时候程序能安全运行退出</p>
<pre><code>1264 ± : go test map_test/map_performance_test.go -v           ⏎ [3h1m] ✹ ✚ ✭
=== RUN   TestOriginMaps
--- PASS: TestOriginMaps (0.00s)
    map_performance_test.go:71: Get 0 = 0
    map_performance_test.go:71: Get 1 = 1
......
    map_performance_test.go:71: Get 99 = 9801
PASS
ok      command-line-arguments  0.339s
</code></pre><p>但是一旦我们把 Writer 数量改为 2</p>
<pre><code>1264 ± : go test map_test/map_performance_test.go -v             [3h2m] ✹ ✚ ✭
=== RUN   TestOriginMaps
fatal error: concurrent map writes

goroutine 21 [running]:
</code></pre><p>立马就爆炸了。那么？？？？golang 自己官方心理没数么？</p>
<p>当然有数 golang 开发者其中之一可是拿图灵奖的。你可以点击<a href="https://stackoverflow.com/questions/11063473/map-with-concurrent-access" title="stackoverflow这里">stackoverflow 上的讨论</a>和<a href="https://github.com/golang/go/issues/21035" title="github 上的讨论">github 这里</a>去查看相关的 issue</p>
<h2 id="syncmap">Sync.Map</h2>
<p>这是某大佬提出的解决方案，我们试试</p>
<pre><code>type SyncMap struct {
    m sync.Map
}

func NewSyncMap() *SyncMap {
    return &amp;SyncMap{}
}

func (o *SyncMap) Get(key interface{}) (interface{}, bool) {
    v, ok := o.m.Load(key)
    return v, ok
}
func (o *SyncMap) Set(key interface{}, value interface{}) {
    o.m.Store(key, value)
}

func (o *SyncMap) Del(key interface{}) {
    o.m.Delete(key)
}
</code></pre><p>我简单封装了一下，测试个性能没啥问题。</p>
<p>现在把 Write 增加也没问题了，可是真的没问题么？</p>
<p>我们现在小改一下第一种 map 加了个 RW 锁，然后和这种 map 做一下比较看看？</p>
<pre><code>type OriginWithRWLock struct {
    m map[interface{}]interface{}
    l sync.RWMutex
}

func NewOriginWithRWLock() *OriginWithRWLock {
    return &amp;OriginWithRWLock{
        m: make(map[interface{}]interface{}),
        l: sync.RWMutex{},
    }
}

func (o *OriginWithRWLock) Get(key interface{}) (interface{}, bool) {
    o.l.RLock()
    v, ok := o.m[key]
    o.l.RUnlock()
    return v, ok
}
func (o *OriginWithRWLock) Set(key interface{}, value interface{}) {
    o.l.Lock()
    o.m[key] = value
    o.l.Unlock()
}

func (o *OriginWithRWLock) Del(key interface{}) {
    o.l.Lock()
    delete(o.m, key)
    o.l.Unlock()
}
</code></pre><p>然后我们这次用 Test 里的 Benchmark 试试看</p>
<pre><code>func BenchmarkMaps(b *testing.B) {

    b.Logf(&quot;Writer: %d,Reader: %d&quot;, Writer, Readers)
    b.Run(&quot;map with SyncLock&quot;, func(b *testing.B) {
        hm := NewSyncMap()
        benchmarkMap(b, hm)
    })

    b.Run(&quot;map with RWLock&quot;, func(b *testing.B) {
        hm := NewOriginWithRWLock()
        benchmarkMap(b, hm)
    })
}

func benchmarkMap(b *testing.B, hm Map) {
    for i := 0; i &lt; b.N; i++ {
        var wg sync.WaitGroup
        for j := 0; j &lt; Writer; j++ {
            wg.Add(1)
            go func() {
                for k := 0; k &lt; 100; k++ {
                    hm.Set(strconv.Itoa(k), k*k)
                    hm.Set(strconv.Itoa(k), k*k)
                    hm.Del(strconv.Itoa(k))
                }
                wg.Done()
            }()
        }
        for j := 0; j &lt; Readers; j++ {
            wg.Add(1)
            go func() {
                for k := 0; k &lt; 100; k++ {
                    hm.Get(strconv.Itoa(k))
                }
                wg.Done()
            }()
        }
    }
}
</code></pre><p>首先是 BenchMark 的函数当使用</p>
<p><code>go test .... -bench=.</code></p>
<p>的时候会被调用，然后来测试两种 Map 性能，上面那个是测试性能的函数，分别对两个函数的进行测试~~拭目以待</p>
<blockquote>
<p>当两者都是 100 的时候</p>
</blockquote>
<pre><code>1266 ± : go test map_test/map_performance_test.go -bench=. -v                                                                                       [3h29m] ✹ ✚ ✭
goos: darwin
goarch: amd64
BenchmarkMaps/map_with_SyncLock-8                   1459            749699 ns/op
BenchmarkMaps/map_with_RWLock-8                     1688           1405360 ns/op
--- BENCH: BenchmarkMaps
    map_performance_test.go:92: Writer: 100,Reader: 100
PASS
ok      command-line-arguments  4.674s
</code></pre><p>然后我测试了，各种情况下的比较画了个表格 单位是 ns/op ,每次操作需要的秒数</p>
<table>
<thead>
<tr>
<th>R/W</th>
<th>map_with_SyncLock</th>
<th>map_with_RWLock</th>
</tr>
</thead>
<tbody>
<tr>
<td>100/100</td>
<td>749699</td>
<td>1405360</td>
</tr>
<tr>
<td>10/100</td>
<td>2053942</td>
<td>333406</td>
</tr>
<tr>
<td>100/10</td>
<td>432235</td>
<td>1076423</td>
</tr>
<tr>
<td>100/200</td>
<td>1474482</td>
<td>1920310</td>
</tr>
<tr>
<td>200/100</td>
<td>1135713</td>
<td>2372780</td>
</tr>
</tbody>
</table>
<p>那么从这个表里可以看出，SyncMap 的整体性能是优于 mapWithRWLock 的我来分析一下为什么</p>
<p>从古至今，人们一直在时间和空间上做斗争，这次也不例外，两种锁的实现原理不一样。</p>
<p><img src="https://imgkr.cn-bj.ufileos.com/8ce8ae6e-8cee-421c-8d54-0057d287e448.png" alt=""></p>
<p>当我们使用普通 Map 带 RWMutex 会将整块内存锁住，然后其他请求就要等待。 SyncMap 是如何实现的呢？</p>
<p><img src="https://imgkr.cn-bj.ufileos.com/9fd62de8-9760-47d5-a525-eb043b409a0c.png" alt=""></p>
<p>它分为两块内存（存的都是指针），一块只读区域，一块 Dirty 区域支持读写。</p>
<p>两边的指针指向原数据，当需要 Get 的时候他会执行<code>Load</code>操作从 Read 中去获取指针指向的值，如果没有找到（<code>miss</code>）发生了，就转而会去 dirty 中获得数据并且存入 Read 中。</p>
<p>当<code>miss</code>超过一定数量的时候，他就会用原子操作把 dirty 的数据 Promote 到 ReadOnly 中。</p>
<p>因此 Sync 这种机制，往往只适用于 Key-Value 相对稳定的业务情况，读多写少的业务。</p>
<blockquote>
<p>手痒想写个内存的看看到底多花多少内存 go tool pprof 是一个工具可以查看代码测评产生的内存日志</p>
</blockquote>
<pre><code>go test map_test/map_performance_test.go -bench=. -memprofile=mem.prof
go tool pprof map.test mem.prof
(pprof)top
...
      flat  flat%   sum%        cum   cum%
    1.54GB 57.95% 57.95%     1.57GB 59.14%  command-line-arguments_test.benchmarkMap.func2
    0.43GB 16.22% 74.17%     0.69GB 25.94%  command-line-arguments_test.benchmarkMap.func1
</code></pre><p>不用说了这看起来三倍的内存消耗，果然越快内存越大。那么？本次测评到此结束？</p>
<p>！！！ 并没有！！！ 还有一个大佬写了个 concurrent-map 甚叼，我们来观摩一波。<a href="https://github.com/orcaman/concurrent-map" title="concurrent-map">concurrent-map</a></p>
<p>立马封装一波</p>
<pre><code>type ConCurrentMap struct {
    m cmap.ConcurrentMap
}

func NewConCurrentMap() *ConCurrentMap {
    conMap := cmap.New()
    return &amp;ConCurrentMap{m: conMap}
}

type OriginMap struct {
    m map[interface{}]interface{}
}

func (c *ConCurrentMap) Get(key interface{}) (interface{}, bool) {
    v, ok := c.m.Get(key.(string))
    return v, ok
}
func (c *ConCurrentMap) Set(key interface{}, value interface{}) {
    c.m.Set(key.(string), value)
}

func (c *ConCurrentMap) Del(key interface{}) {
    c.m.Remove(key.(string))
}
</code></pre><p>迫不及待开始测试，当 Write=100，Reader=100 的时候</p>
<pre><code>1271 ± : go test map_test/map_performance_test.go -bench=. -v                                                                                       [4h11m] ✹ ✚ ✭
goos: darwin
goarch: amd64
BenchmarkMaps/map_with_SyncLock-8                   1374            760728 ns/op
BenchmarkMaps/map_with_RWLock-8                     1671           1556679 ns/op
BenchmarkMaps/concurrentMap-8                       2667           1060736 ns/op
--- BENCH: BenchmarkMaps
    map_performance_test.go:114: Writer: 100,Reader: 100
</code></pre><p>那么我同样做个表格吧，把读写的几种情况都列出来</p>
<table>
<thead>
<tr>
<th>R/W</th>
<th>map_with_SyncLock</th>
<th>map_with_RWLock</th>
<th>concurrentMap</th>
</tr>
</thead>
<tbody>
<tr>
<td>100/100</td>
<td>760728</td>
<td>1556679</td>
<td>1060736</td>
</tr>
<tr>
<td>10/100</td>
<td>1690986</td>
<td>338078</td>
<td>457065</td>
</tr>
<tr>
<td>100/10</td>
<td>443063</td>
<td>1026160</td>
<td>544635</td>
</tr>
</tbody>
</table>
<p>最后说一下这个并发读map是怎么搞的</p>
<p><img src="https://imgkr.cn-bj.ufileos.com/84240d5a-21a9-4df7-8cf6-97fd06dc2b11.png" alt=""></p>
<p>左边是普通的map，当有读写的时候锁上了，其他线程就无法读写了。右边的是<code>concurrentMap</code>，他利用了一种<code>partition</code>的思想，把<code>Map</code>的内存<code>SHARD</code>（分割）成N份，然后用不同的🔐锁上锁，那么降低了需要资源被锁的概率。</p>
<p>我们在日常中编程的时候容易陷入一种误区，就是这锁，那锁，全锁上，面试也在问各种锁，但是在真实QPS比价高的业务中，锁是一种很可怕的东西，如果能在编程的时候好好想想写出<code>LockFree</code>的程序是最好的啊，例如golang1.13起还是哪个版本忘了，标准包sync.pool就去掉了锁，下次讲讲他是怎么实现的吧。</p>
<p>我是北京某211的混子，从19年10月开始写两行golang到现在不知不觉已经过去了2个月，上手就开始拉框架写代码的我已经进化到开始分析性能，然后优化代码啦，如果有小伙伴想一起讨论讨论，欢迎。</p>

    </article>
</div>


        
<nav role="navigation" class="flex-container bottom-menu">
    
<hr />
<p>


    
        <a href="/posts">back</a>
        
            &#183;
        
    

    
        
            <a href="/posts">我的文章</a>
        
    
    
        
            &#183; 
            <a href="/gallery">画册</a>
        
            &#183; 
            <a href="/portfolio">珍贵时光</a>
        
            &#183; 
            <a href="/about">关于我</a>
        
    
    &#183; 
    <a href="/">
        main
    </a>

</p>
</nav>

    </main>
    
    <footer class="flex-container footer">I will code for a long time. Would you?</footer>
    
    
</body>

</html>